<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Ammo.js btGImpactMeshShape demo</title>
    <style>
        body {
            color: #61443e;
            font-family: Monospace;
            font-size: 13px;
            text-align: center;

            background-color: #bfd1e5;
            margin: 0px;
            overflow: hidden;
        }

        #info {
            position: absolute;
            top: 0px;
            width: 100%;
            padding: 5px;
        }

        a {
            color: #a06851;
        }
    </style>
</head>

<body>
    <div id="info">Ammo.js btGImpactMeshShape demo</div>
    <div id="container"></div>

    <script src="../../builds/ammo.js"></script>

    <script src="../js/three/three.min.js"></script>
    <script src="../js/three/OrbitControls.js"></script>
    <script src="../js/three/GLTFLoader.js"></script>
    <script src="../js/three/Detector.js"></script>
    <script src="../js/three/stats.min.js"></script>

    <script>

        const gltfLoader = new THREE.GLTFLoader();
        const textureLoader = new THREE.TextureLoader();

        Ammo().then(function (Ammo) {

            // Detects webgl
            if (!Detector.webgl) {
                Detector.addGetWebGLMessage();
                document.getElementById('container').innerHTML = "";
            }

            // - Global variables -

            // Graphics variables
            let container, stats;
            let camera, controls, scene, renderer;
            let texture;
            let clock = new THREE.Clock();

            // Physics variables
            let physicsWorld;
            const margin = 0.01;
            const rigidBodies = [];
            let pos = new THREE.Vector3();
            let quat = new THREE.Quaternion();
            let transformAux1;
            let tempBtVec3_1;

            // Models
            let urls = [
                '../models/chain.glb'
            ];

            // - Main code -
            init();
            animate();

            function init() {

                initGraphics();

                initPhysics();

                createObjects();

            }

            function initGraphics() {

                container = document.getElementById('container');

                scene = new THREE.Scene();
                scene.background = new THREE.Color(0xbfd1e5);

                renderer = new THREE.WebGLRenderer();
                renderer.setPixelRatio(window.devicePixelRatio);
                renderer.setSize(window.innerWidth, window.innerHeight);
                renderer.shadowMap.enabled = true;
                container.appendChild(renderer.domElement);

                camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.2, 2000);
                camera.position.set(-40, 34, 30);

                controls = new THREE.OrbitControls(camera, renderer.domElement);
                controls.target.y = 10;
                controls.update();

                const ambientLight = new THREE.AmbientLight(0x404040);
                scene.add(ambientLight);

                const light = new THREE.DirectionalLight(0xffffff, 1);
                light.position.set(- 10, 10, 5);
                light.castShadow = true;
                const d = 10;
                light.shadow.camera.left = - d;
                light.shadow.camera.right = d;
                light.shadow.camera.top = d;
                light.shadow.camera.bottom = - d;

                light.shadow.camera.near = 2;
                light.shadow.camera.far = 50;

                light.shadow.mapSize.x = 1024;
                light.shadow.mapSize.y = 1024;

                scene.add(light);

                stats = new Stats();
                stats.domElement.style.position = 'absolute';
                stats.domElement.style.top = '0px';
                container.appendChild(stats.domElement);

                window.addEventListener('resize', onWindowResize);

            }

            function initPhysics() {

                // Physics configuration
                // ---------------------

                let m_collisionConfiguration = new Ammo.btDefaultCollisionConfiguration();
                let m_dispatcher = new Ammo.btCollisionDispatcher(m_collisionConfiguration);
                let m_broadphase = new Ammo.btDbvtBroadphase();
                let m_constraintSolver = new Ammo.btSequentialImpulseConstraintSolver();

                physicsWorld = new Ammo.btDiscreteDynamicsWorld(m_dispatcher, m_broadphase, m_constraintSolver, m_collisionConfiguration);
                physicsWorld.setGravity(new Ammo.btVector3(0, -9.810, 0));

                Ammo.btGImpactCollisionAlgorithm.prototype.registerAlgorithm(physicsWorld.getDispatcher());

                transformAux1 = new Ammo.btTransform();
                tempBtVec3_1 = new Ammo.btVector3(0, 0, 0);

            }

            function loadModels(urls) {
                urls.forEach(url => {
                    gltfLoader.load(
                        url,
                        (gltf) => {

                            scene.add(gltf.scene);

                            gltf.scene.traverse((child) => {
                                if (child.type == 'Mesh') {
                                    child.castShadow = true;
                                    child.receiveShadow = true;
                                    child.material.side = THREE.FrontSide;

                                    let trimesh = createGImpactCollision(child);

                                    pos.copy(child.position);
                                    quat.copy(child.quaternion);

                                    if (child.name == 'Static') {
                                        let body = createRigidBody(child, trimesh, 0, pos, quat);
                                        body.setCollisionFlags(body.getCollisionFlags() | 1/* btCollisionObject:: CF_STATIC_OBJECT */);
                                    } else {
                                        createRigidBody(child, trimesh, 1, pos, quat);
                                    }

                                }
                            })
                        }
                    )
                });
            }

            function createObjects() {

                // Ground
                pos.set(0, - 0.5, 0);
                quat.set(0, 0, 0, 1);
                const ground = createParalellepipedWithPhysics(40, 1, 40, 0, pos, quat, new THREE.MeshPhongMaterial({ color: 0xFFFFFF }));
                scene.add(ground);
                ground.receiveShadow = true;
                textureLoader.load('../textures/grid.png', function (texture) {

                    texture.wrapS = THREE.RepeatWrapping;
                    texture.wrapT = THREE.RepeatWrapping;
                    texture.repeat.set(40, 40);
                    ground.material.map = texture;
                    ground.material.needsUpdate = true;

                });

                // Wall
                let brickMass = 0.5;
                let brickLength = 2;
                let brickDepth = 1;
                let brickHeight = brickLength * 0.5;
                let numBricksLength = 6;
                let numBricksHeight = 8;
                let z0 = - numBricksLength * brickLength * 0.5;
                pos.set(0, brickHeight * 0.5, z0);
                quat.set(0, 0, 0, 1);
                for (let j = 0; j < numBricksHeight; j++) {

                    let oddRow = (j % 2) == 1;

                    pos.z = z0;

                    if (oddRow) {
                        pos.z -= 0.25 * brickLength;
                    }

                    let nRow = oddRow ? numBricksLength + 1 : numBricksLength;
                    for (let i = 0; i < nRow; i++) {

                        let brickLengthCurrent = brickLength;
                        let brickMassCurrent = brickMass;
                        if (oddRow && (i == 0 || i == nRow - 1)) {
                            brickLengthCurrent *= 0.5;
                            brickMassCurrent *= 0.5;
                        }

                        let brick = createParalellepiped(brickDepth, brickHeight, brickLengthCurrent, brickMassCurrent, pos, quat, createMaterial());
                        brick.castShadow = true;
                        brick.receiveShadow = true;
                        scene.add(brick);

                        if (oddRow && (i == 0 || i == nRow - 2)) {
                            pos.z += 0.75 * brickLength;
                        }
                        else {
                            pos.z += brickLength;
                        }

                    }
                    pos.y += brickHeight;
                }

                // Chain
                loadModels(urls);

            }

            function createParalellepipedWithPhysics(sx, sy, sz, mass, pos, quat, material) {

                const object = new THREE.Mesh(new THREE.BoxGeometry(sx, sy, sz, 1, 1, 1), material);
                const shape = new Ammo.btBoxShape(new Ammo.btVector3(sx * 0.5, sy * 0.5, sz * 0.5));
                shape.setMargin(margin);

                createRigidBody(object, shape, mass, pos, quat);

                return object;

            }

            function createParalellepiped(sx, sy, sz, mass, pos, quat, material) {

                let threeObject = new THREE.Mesh(new THREE.BoxGeometry(sx, sy, sz, 1, 1, 1), material);
                let shape = new Ammo.btBoxShape(new Ammo.btVector3(sx * 0.5, sy * 0.5, sz * 0.5));
                shape.setMargin(margin);

                createRigidBody(threeObject, shape, mass, pos, quat);

                return threeObject;

            }

            function createRandomColor() {

                return Math.floor(Math.random() * (1 << 24));

            }

            function createMaterial(color) {

                color = color || createRandomColor();
                return new THREE.MeshPhongMaterial({ color: color });

            }

            function createRigidBody(object, physicsShape, mass, pos, quat, vel, angVel) {

                if (pos) {

                    object.position.copy(pos);

                } else {

                    pos = object.position;

                }

                if (quat) {

                    object.quaternion.copy(quat);

                } else {

                    quat = object.quaternion;

                }

                const transform = new Ammo.btTransform();
                transform.setIdentity();
                transform.setOrigin(new Ammo.btVector3(pos.x, pos.y, pos.z));
                transform.setRotation(new Ammo.btQuaternion(quat.x, quat.y, quat.z, quat.w));
                const motionState = new Ammo.btDefaultMotionState(transform);

                const localInertia = new Ammo.btVector3(0, 0, 0);
                physicsShape.calculateLocalInertia(mass, localInertia);

                const rbInfo = new Ammo.btRigidBodyConstructionInfo(mass, motionState, physicsShape, localInertia);
                const body = new Ammo.btRigidBody(rbInfo);

                body.setFriction(0.5);

                if (vel) {

                    body.setLinearVelocity(new Ammo.btVector3(vel.x, vel.y, vel.z));

                }

                if (angVel) {

                    body.setAngularVelocity(new Ammo.btVector3(angVel.x, angVel.y, angVel.z));

                }

                object.userData.physicsBody = body;
                object.userData.collided = false;

                if (mass > 0) {

                    rigidBodies.push(object);

                    // Disable deactivation
                    body.setActivationState(4);

                }

                physicsWorld.addRigidBody(body);

                return body;

            }


            function getMeshData(mesh) {

                const index = mesh.geometry.index !== null ? mesh.geometry.index : undefined;
                const attributes = mesh.geometry.attributes;
                const scale = mesh.scale;

                if (attributes.position === undefined) {

                    console.error('getMeshData(): Position attribute required for conversion.');
                    return;

                }

                const position = attributes.position;

                let vertices = [];
                let faces = [];

                for (let i = 0; i < position.count; i++) {

                    vertices.push({
                        x: scale.x * position.getX(i),
                        y: scale.y * position.getY(i),
                        z: scale.z * position.getZ(i)
                    });

                }

                if (index !== undefined) {

                    for (let i = 0; i < index.count; i += 3) {

                        faces.push({
                            a: index.getX(i),
                            b: index.getX(i + 1),
                            c: index.getX(i + 2)
                        });

                    }

                } else {

                    for (let i = 0; i < position.count; i += 3) {

                        faces.push({
                            a: i,
                            b: i + 1,
                            c: i + 2
                        });

                    }
                }

                return {
                    vertices,
                    faces
                }
            }


            function createGImpactCollision(mesh, scale) {

                let faces, vertices;
                let totvert = 0;

                if (mesh.isMesh) {
                    let data = getMeshData(mesh);

                    faces = data.faces;
                    vertices = data.vertices;

                    totvert = vertices.length;

                }
                else {
                    console.error("cannot make mesh shape for non-Mesh object");
                }

                if (totvert == 0) {
                    console.error("no vertices to define mesh shape with");
                }

                if (!scale)
                    scale = { x: 1, y: 1, z: 1 };

                /* vertices, faces */
                let ammoMesh = new Ammo.btTriangleMesh();

                for (let i = 0, l = faces.length; i < l; i++) {
                    let a = faces[i].a;
                    let b = faces[i].b;
                    let c = faces[i].c;
                    ammoMesh.addTriangle(
                        new Ammo.btVector3(vertices[a].x, vertices[a].y, vertices[a].z),
                        new Ammo.btVector3(vertices[b].x, vertices[b].y, vertices[b].z),
                        new Ammo.btVector3(vertices[c].x, vertices[c].y, vertices[c].z),
                        false
                    );
                }

                let triangleShape = new Ammo.btGImpactMeshShape(ammoMesh);
                triangleShape.setMargin(0.01);
                triangleShape.setLocalScaling(new Ammo.btVector3(scale.x, scale.y, scale.z));
                triangleShape.updateBound();

                return triangleShape;

            }

            function updatePhysics(deltaTime) {

                // Step world
                physicsWorld.stepSimulation(deltaTime, 10);

                // Update rigid bodies
                for (let i = 0, il = rigidBodies.length; i < il; i++) {

                    const objThree = rigidBodies[i];
                    const objPhys = objThree.userData.physicsBody;
                    const ms = objPhys.getMotionState();

                    if (ms) {

                        ms.getWorldTransform(transformAux1);
                        const p = transformAux1.getOrigin();
                        const q = transformAux1.getRotation();
                        objThree.position.set(p.x(), p.y(), p.z());
                        objThree.quaternion.set(q.x(), q.y(), q.z(), q.w());

                        objThree.userData.collided = false;

                    }

                }
            }

            function onWindowResize() {

                camera.aspect = window.innerWidth / window.innerHeight;
                camera.updateProjectionMatrix();

                renderer.setSize(window.innerWidth, window.innerHeight);

            }

            function animate() {

                requestAnimationFrame(animate);

                render();
                stats.update();

            }

            function render() {

                const deltaTime = clock.getDelta();

                updatePhysics(deltaTime);

                renderer.render(scene, camera);

            }

        });

    </script>

</body>

</html>